<?php
/**
* Mapping from a table + field to a class + property
* currently only supports 1 class per 1 table
* 
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* 
* @copyright Copyright (c) 2008-2009 Ne8, LLC <josh.ribakoff@gmail.com>
* @license New BSD License
* @version 0.1
* @category Shuffler
* @package Shuffler_Mapper
* @subpackage Mapping
* 
* @todo subclass for association table mapping
*/
abstract class Shuffler_Mapper_Mapping
{
    /**
    * @var Shuffler_Mapper
    */
    protected $sourceMapper, $targetMapper;
    
    /**
    * @var Shuffler_Inflector
    */
    protected $inflector;
    
    /**
    * @var string name of field for this mapping
    */
    protected $field;
    
    /**
    * @var boolean wether or not this field is immutable, wether to pass in constructor or via setter / adder
    */
    protected $immutable = false;
    
    /**
    * @var string name of class this mapping is for
    */
    protected $class;
    
    /**
    * @var alias for property name to use when reflectively calling accessors / mutators
    */
    protected $property;
    
    
    protected $fk;
    
    /**
    * @var bool wether or not this mapping is for a field on the main ( "source" ) mapper's table
    */
    protected $fieldIsInline = false;
    
    /**
    * @var bool wether or not this mapping is lazy loading
    */
    protected $lazy_load = false;
           
    /**
    * Create a mapping
    *
    * @param Shuffler_Mapper source data mapper
    * @param string name of database field for this mapping
    * @param string name of class for this mapping
    * @param array additional options
    */
    public function __construct(
        Shuffler_Mapper $sourceMapper = NULL,
        array $options = array()
    )
    {
        $this->inflector = new Shuffler_Inflector();
        $this->sourceMapper = $sourceMapper;
        $this->setOptions( $options );
    }
    
    public function setOptions( $options )
    {
        if( isset( $options['immutable'] ) )
        {
            if( !is_bool( $options['immutable'] ) )
            {
                throw new Exception( '$immutable must be a boolean' );
            }
            $this->immutable = $options['immutable'];
        }
        foreach( $options as $option => $value )
        {
            $command = 'set' . ucfirst( $option );
            if( method_exists( $this, $command ) )
            {
                $this->$command( $value );
            }
            else
            {
                $this->$option = $value;
            }
        }
        return $this;
    }
    
    /**
    * Get the class name for this mapping
    * 
    * @return string name of class
    */
    public function getClass()
    {
        return $this->class;
    }
    
    /**
    * Get the database field name for this mapping
    * 
    * @return string name of field
    */
    public function getField()
    {
        return $this->field;
    }
    
    /**
    * Set the database field name for this mapping
    * 
    * @param string name of field
    */
    public function setField( $field )
    {
        $this->field = $field;
        return $this;
    }
    
    /**
    * Get the [aliased] database field name for this mapping
    * 
    * @return string name of mapping
    */
    public function getAliasedField( $field = null )
    {
        return $this->getTableName() . '_' . ( is_null( $field ) ? $this->getField() : $field );
    }
    
    /**
    * Get the class property name for this mapping
    * 
    * used to derive name of setters, getters, adders, etc..
    * 
    * @return string name of property for class
    */
    public function getProperty()
    {
        if( $this->hasPropertyOverride() )
        {
            return $this->property;
        }
        return $this->inflector->getPropertyFromClass( $this->class );
    }
    
    public function hasPropertyOverride()
    {
        if( isset( $this->property ) && !is_null( $this->property ) )
        {
            return true;
        }
        return false;
    }
    
    /**
    * Get the name of the table for this mapping
    * 
    * @return string name of table
    */
    public function getTableName()
    {
         return $this->sourceMapper->getTableName( $this->getClass() );
    }
    
    /**
    * Is this mapping immutable / should it be set in the constructor & not via a setter.
    */
    public function isImmutable()
    {
        return $this->immutable;
    }
    
    /**
    * Load method, delegates to strategy method
    * 
    * @param Shuffler_Model to inject data into
    * @param result set from data mapper
    * @return Shuffler_Model after injecting data from this mapping
    */
    public function load( Shuffler_Model $model, array $rs = null )
    {
        // if this data was set in the constructor
        if( $this->isImmutable() )
        {    
            return $model;
        }
        return $this->doLoad( $model, $rs );
    }
    
    /**
    * template method for performing the mapping of data to the model
    * 
    * @param Shuffler_Model to inject data into
    * @return Shuffler_Model
    */
    protected function doLoad( Shuffler_Model $model, array $rs = null )
    {
        // default to just return it without doing anything
        return $model; 
    } 
    
    public function save( Shuffler_Model $model ) { }
    
    public function getAccessorMethod()
    {
        return 'get' . $this->getCommandSuffix();
    }
    
    public function getMutatorMethod()
    {
        return 'set' . $this->getCommandSuffix();
    }
    
    public function getCommandSuffix()
    {
        return $this->inflector->getCommandSuffixFromProperty( $this->getProperty() ); 
    } 
    
    abstract public function getType();

    /**
    * Get a field from the row by name
    * 
    * @param array associative row array
    * @param string name of field to get
    * 
    * @return string
    */
    protected function getRowField( array $row, $fieldName )
    {
        if( !is_array( $row ) )
        {
            throw new Exception( 'row was not array, ' . gettype( $row ) . ' given' );
        }
        if( isset( $row[ $fieldName ] ) )
        {
            return $row[ $fieldName ];
        }
    }
    
    /**
    * Getter strategy for Shuffler_Mapper->getFieldValues()
    * Any fields that are inline / "embedded" in the main table are inserted / updated
    * in one SQL call, this method is called for each mapping for this select. For more complicated mappings
    * the saving is infact handled in the save() method of this class
    * 
    * @param Shuffler_Model to get data from
    * @return return value from model's getter method
    * @todo refactor to simplify
    */
    public function getFieldValue( Shuffler_Model $model )
    {
        $command = $this->getAccessorMethod();
        if( !method_exists( $model, $command ) )
        {
            throw new Exception( 'Method ' . get_class( $model ) . '->' . $command . ' does\'t exist' );
        }
        $foreignModel = $model->$command();
        if( !( $foreignModel instanceof Shuffler_Model ) && !( $foreignModel instanceof Shuffler_Collection ) )
        {
            throw new Exception( 'Method ' . get_class( $model ) . '->' . $command . ' returned ' . gettype( $foreignModel ) . ', Shuffler_Model or Shuffler_Collection expected' );
        }
        return $this->doGetFieldValue( $foreignModel ); 
    }
    
    protected function doGetFieldValue( $val )
    {
        return $val; 
    }
    
    /**
    * Get the associated model for a given id
    * 
    * @return Shuffler_Model | Shuffler_Model that implements Shuffler_Null if no value passed
    */
    public function getAssociatedModel( $id )
    {
        if( !is_int( $id ) )
        {
            throw new Exception( 'id must be of type int,' . gettype( $id ) . ' given'  );
        }
        $mapper = $this->getTargetMapper()->getMapper();
        if( !(int)$id )
        {
            return $mapper->createNullModel();
        }
        return $mapper->find( $id );
    }
    
    protected function getTargetMapper()
    {
        if( isset( $this->targetMapper ) )
        {
            return $this->targetMapper;
        }
        $mapper = $this->sourceMapper->getMapper( $this->getClass() );
        if( !$mapper instanceof Shuffler_Mapper )
        {
            throw new Exception( '!$mapper instanceof Shuffler_Mapper' );
        }
        $this->targetMapper = $mapper;
        return $mapper;
    }
    
    public function setTargetMapper( Shuffler_Mapper $mapper )
    {
        $this->targetMapper = $mapper;
    }
    
    /**
    * @todo move method up / extract class
    **/
    public function getValue( $row )
    {
        $field = $this->getAliasedField();
        return $this->getAssociatedModel( $this->getRowField( $row, $field ) );
    }
    
    /**
    * Get the mapper for this mapping
    * 
    * @return Shuffler_Mapper that was passed at initialization
    */
    public function getMapper( $modelName = NULL )
    {
        if( $modelName == NULL )
        {
            $modelName = $this->getClass();
        }
        return Shuffler_Factory::getInstance()->getMapper( $modelName );
    }
    
    public function select( $select )
    {
        return $select;
    }
    
    public function getReadAdapter()
    {
        return $this->sourceMapper->getReadAdapter(); 
    }
    
    public function getWriteAdapter()
    {
        return $this->sourceMapper->getWriteAdapter(); 
    }
    
    function fieldIsInline()
    {
        return $this->fieldIsInline;
    }
    
    /**
    * @return bool lazy_load
    */
    public function getLazyLoad()
    {
        return $this->lazy_load;
    }
    
    /**
    * @param bool lazy_load
    */
    public function setLazyLoad( $lazy_load )
    {
        $this->lazy_load = $lazy_load;
        return $this;
    }
}